package demo;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import de.d3fact.common.events.Event;
import de.d3fact.common.events.IListener;
import de.d3fact.mfslib.buffer.elective.AndElectiveBuffer;
import de.d3fact.mfslib.processor.Processor;
import de.d3fact.mfslib.sink.Sink;
import de.d3fact.mfslib.source.Source;
import de.d3fact.mfsmodel.TokenTypeConfig;
import de.d3fact.modelapi.Model;
import de.d3fact.modelapi.ModelGlobals;
import de.d3fact.modelapi.dataport.DataportConfig;
import de.d3fact.modelapi.dataport.IConnectorport;
import de.d3fact.modelapi.dataport.IDataport;
import de.d3fact.modelapi.dataport.component.IsActiveDataport;
import de.d3fact.modelapi.dataport.pull.DoublePullInputport;
import de.d3fact.modelapi.dataport.pull.DoublePullOutputport;
import de.d3fact.modelapi.dataport.pull.LongPullInputport;
import de.d3fact.modelapi.dataport.pull.LongPullOutputport;
import de.d3fact.modelapi.dataport.push.IntPushInputport;
import de.d3fact.modelapi.dataport.push.LongPushInputport;
import de.d3fact.modelapi.scheduler.Scheduler;
import de.d3fact.scl.math.ValueConverter;
import de.d3fact.scl.math.rng.distributions.continuous.Uniform;
import de.d3fact.scl.math.rng.generators.MersenneRNG;

/**
 * This Model simulates a serial connection of 4 Terminals with uniform distributed processing time
 * in the interval of [40,60]. The Source creates Tasks in a random interval of [30,90].
 * 
 * @author cgrote
 */

public class Terminal
{

	private Scheduler scheduler = new Scheduler();
	private final static int TASKS = 20;
	private final static int TERMINALS = 4;
	private final static boolean VERBOSE_OUTPUT = true;
	private final static boolean WRITE_CSV = false;

	private static int recieved_tasks = 0;
	private static long[][] time = new long[3 + TERMINALS][TASKS];
	private static long[][] randomTime = new long[TERMINALS][TASKS];
	private static long service_time = 0;
	private static long input_rate = 0;
	private static int terminal_count = 0;
	private static int random_count = 0;
	private static Model model;

	public Terminal() throws IOException
	{

		IListener sourceListener = new IListener()
			{
				private int generated_tasks = 0;

				@Override
				public void handleEvent(Event e)
				{

					generated_tasks++;

					String output = "Task: " + generated_tasks + "  Starttime: " + scheduler.currentTime() + " ";

					time[0][generated_tasks - 1] = scheduler.currentTime();

					if (generated_tasks > 1)
					{
						input_rate += time[0][generated_tasks - 1] - time[0][generated_tasks - 2];
						output += " Distance: " + (time[0][generated_tasks - 1] - time[0][generated_tasks - 2]);
					}

					if (VERBOSE_OUTPUT)
					{
						System.out.println(output);
					}

					if (generated_tasks == TASKS)
					{
						// Generate no more new Token
						((Source)model.getComponent("Source")).<IsActiveDataport> getInputport("is Active").directValue(false);
					}

				}

			};

		IListener sinkListener = new IListener()
			{
				@Override
				public void handleEvent(Event e)
				{
					String output;
					recieved_tasks++;

					time[1][recieved_tasks - 1] = scheduler.currentTime();
					time[2][recieved_tasks - 1] = time[1][recieved_tasks - 1] - time[0][recieved_tasks - 1];
					service_time += time[2][recieved_tasks - 1];

					if (VERBOSE_OUTPUT)
					{
						output = "Task: " + recieved_tasks + "  Endtime: " + scheduler.currentTime();

						if (recieved_tasks > 1)
						{
							output += "  Distance: " + (time[1][recieved_tasks - 1] - time[1][recieved_tasks - 2]);
						}

						System.out.println(output + "  Service time: " + time[2][recieved_tasks - 1]);
					}
				}

			};

		model = new Model(scheduler);

		// Random Number Generation
		MersenneRNG rng1 = new MersenneRNG("Mersenne1", model);
		rng1.<LongPushInputport> getPort("input_seed", IDataport.DIRECTION_INPUT).directValue(23);
		MersenneRNG rng2 = new MersenneRNG("Mersenne2", model);
		rng2.<LongPushInputport> getPort("input_seed", IDataport.DIRECTION_INPUT).directValue(17);

		// Uniform distribution
		Uniform uniDis = new Uniform("uniform", model);
		uniDis.<DoublePullInputport> getPort("input_rng", IDataport.DIRECTION_INPUT).connectTo(
			rng1.getPort("output_rng", IDataport.DIRECTION_OUTPUT));
		uniDis.<DoublePullInputport> getPort("input-a", IDataport.DIRECTION_INPUT).directValue(40d);
		uniDis.<DoublePullInputport> getPort("input_b", IDataport.DIRECTION_INPUT).directValue(60d);
		DoublePullOutputport rng_proseccing_time = uniDis.getPort("output_distr", IDataport.DIRECTION_OUTPUT);
		// output RNG uniform distribution interval [40d,60d]

		// Uniform distribution
		Uniform uniDis2 = new Uniform("uniform2", model);
		uniDis2.<DoublePullInputport> getPort("input_rng", IDataport.DIRECTION_INPUT).connectTo(
			rng2.getPort("output_rng", IDataport.DIRECTION_OUTPUT));
		uniDis2.<DoublePullInputport> getPort("input-a", IDataport.DIRECTION_INPUT).directValue(30d);
		uniDis2.<DoublePullInputport> getPort("input_b", IDataport.DIRECTION_INPUT).directValue(90d);
		DoublePullOutputport rng_creating_time = uniDis2.getPort("output_distr", IDataport.DIRECTION_OUTPUT);
		// output RNG uniform distribution interval [30d,90d]

		// CONVERT creating_time TO LONG
		ValueConverter conv = new ValueConverter("conv", model);
		conv.createInputport("conv_in", new DataportConfig(IDataport.TYPE_CONNECTOR, ModelGlobals.DOUBLE));
		conv.createOutputport("conv_out", new DataportConfig(IDataport.TYPE_CONNECTABLE, ModelGlobals.LONG));
		(conv.<IConnectorport> getInputport("conv_in")).connectTo(rng_proseccing_time);
		LongPullOutputport conv_rng_proseccing_time = conv.<LongPullOutputport> getOutputport("conv_out");

		// CONVERT proseccing_time TO LONG
		ValueConverter conv2 = new ValueConverter("conv2", model);
		conv2.createInputport("conv_in", new DataportConfig(IDataport.TYPE_CONNECTOR, ModelGlobals.DOUBLE));
		conv2.createOutputport("conv_out", new DataportConfig(IDataport.TYPE_CONNECTABLE, ModelGlobals.LONG));
		(conv2.<IConnectorport> getInputport("conv_in")).connectTo(rng_creating_time);
		LongPullOutputport conv_rng_creating_time = conv2.<LongPullOutputport> getOutputport("conv_out");

		Source source = new Source("Source", model);
		AndElectiveBuffer buffer = new AndElectiveBuffer("Buffer", model);

		Processor terminal1 = new Processor("Terminal1", model);
		Processor terminal2 = new Processor("Terminal2", model);
		Processor terminal3 = new Processor("Terminal3", model);
		Processor terminal4 = new Processor("Terminal4", model);

		Sink sink = new Sink("Sink", model);

		// Connections
		buffer.getInputChannels()[0].connectTo(source.getOutputChannels()[0]);
		terminal1.getInputChannels()[0].connectTo(buffer.getOutputChannels()[0]);
		terminal2.getInputChannels()[0].connectTo(terminal1.getOutputChannels()[0]);
		terminal3.getInputChannels()[0].connectTo(terminal2.getOutputChannels()[0]);
		terminal4.getInputChannels()[0].connectTo(terminal3.getOutputChannels()[0]);
		sink.getInputChannels()[0].connectTo(terminal4.getOutputChannels()[0]);

		// Fire and Processing Time
		source.<LongPullInputport> getPort(Source.DATAPORT_FREQUENCY, IDataport.DIRECTION_INPUT).connectTo(conv_rng_creating_time);
		terminal1.<LongPullInputport> getPort(Processor.DATAPORT_PROCESSING_TIME, IDataport.DIRECTION_INPUT).connectTo(
			conv_rng_proseccing_time);
		terminal2.<LongPullInputport> getPort(Processor.DATAPORT_PROCESSING_TIME, IDataport.DIRECTION_INPUT).connectTo(
			conv_rng_proseccing_time);
		terminal3.<LongPullInputport> getPort(Processor.DATAPORT_PROCESSING_TIME, IDataport.DIRECTION_INPUT).connectTo(
			conv_rng_proseccing_time);
		terminal4.<LongPullInputport> getPort(Processor.DATAPORT_PROCESSING_TIME, IDataport.DIRECTION_INPUT).connectTo(
			conv_rng_proseccing_time);

		// Configuration
		TokenTypeConfig ttc01 = new TokenTypeConfig(0, 1);
		source.getOutputChannels()[0].configure(ttc01);

		buffer.getInputChannels()[0].configure(ttc01);
		buffer.getOutputChannels()[0].configure(new TokenTypeConfig(0, Integer.MAX_VALUE - 1));
		buffer.<IntPushInputport> getPort(AndElectiveBuffer.DATAPORT_TOTAL_CAPACITY, IDataport.DIRECTION_INPUT).directValue(
			Integer.MAX_VALUE - 1);

		terminal1.getInputChannels()[0].configure(ttc01);
		terminal1.getOutputChannels()[0].configure(ttc01);
		terminal2.getInputChannels()[0].configure(ttc01);
		terminal2.getOutputChannels()[0].configure(ttc01);
		terminal3.getInputChannels()[0].configure(ttc01);
		terminal3.getOutputChannels()[0].configure(ttc01);
		terminal4.getInputChannels()[0].configure(ttc01);
		terminal4.getOutputChannels()[0].configure(ttc01);

		sink.getInputChannels()[0].configure(ttc01);

		// Listener
		source.getOutputChannels()[0].addListener(sourceListener);
		// buffer.getInputChannels()[0].addListener(bufferListener);
		terminal1.getOutputChannels()[0].addListener(new TimeCollector());
		terminal2.getOutputChannels()[0].addListener(new TimeCollector());
		terminal3.getOutputChannels()[0].addListener(new TimeCollector());
		terminal4.getOutputChannels()[0].addListener(new TimeCollector());
		terminal1.getInputport(Processor.DATAPORT_PROCESSING_TIME).addListener(new RandomDataCollector());
		terminal2.getInputport(Processor.DATAPORT_PROCESSING_TIME).addListener(new RandomDataCollector());
		terminal3.getInputport(Processor.DATAPORT_PROCESSING_TIME).addListener(new RandomDataCollector());
		terminal4.getInputport(Processor.DATAPORT_PROCESSING_TIME).addListener(new RandomDataCollector());
		sink.getInputChannels()[0].addListener(sinkListener);

		scheduler.setStartTime(0);

		// FileDataLocation loc = new FileDataLocation(new File("Terminal.cobj"), new
		// MIMEType("cobj", MIMEType.SUBTYPE_TEXT));
		// LSMConverterDB.registerConverter(new LSMTTCConverter());
		// LSMConverterDB.registerConverter(new LSMDataportConverter());
		// LSMConverterDB.registerConverter(new LSMConnectionObjectConverter());
		// LSMConverterDB.registerConverter(new LSMModelComponentConverter());
		// LSMConverterDB.registerConverter(new LSMModelConverter());
		// (new LSMSaver()).save(loc, model);

		while (scheduler.currentTime() < scheduler.getEndTime() && scheduler.eventCount() > 0)
		{
			scheduler.nextStep();
		}

		System.out.println("\nAverage input rate: " + input_rate / TASKS + " Time-units");
		System.out.println("Average output rate: " + time[1][TASKS - 1] / TASKS + " Time-units");
		System.out.println("Average service time per task: " + service_time / TASKS + " Time-units");

		// generate a .csv with all collected time-data
		if (WRITE_CSV)
			writeData("/tmp/data.csv");

	}

	private static void writeData(String filename)
	{
		String row = "";
		String columns = "Source;Sink;Service-Time";
		for (int i = 0; i < terminal_count; i++)
		{
			columns += ";Terminal" + (i + 1);
		}
		for (int i = 0; i < random_count; i++)
		{
			columns += ";Random-Time" + (i + 1);
		}
		try
		{
			FileWriter data = new FileWriter(new File(filename));

			data.write(columns + ";Distance-Input;Distance-Output" + System.getProperty("line.separator"));

			for (int component = 0, task = 0; task < TASKS; component++)
			{
				if (component == 3 + terminal_count - 1)
				{
					row += time[component][task] + ";";
					for (int i = 0; i < random_count; i++)
					{
						row += randomTime[i][task] + ";";
					}
					if (task == 0)
						row += time[0][task] + ";";
					else
						row += (time[0][task] - time[0][task - 1]) + ";" + (time[1][task] - time[1][task - 1]);

					data.write(row + System.getProperty("line.separator"));
					task++;
					component = -1;
					row = "";
				} else if (component < 3 + terminal_count - 1)
					row += time[component][task] + ";";
			}
			data.close();
		} catch (IOException e)
		{
			e.printStackTrace();
		}
	}

	private class TimeCollector implements IListener
	{

		private int id;
		private int terminal_tasks = 0;

		TimeCollector()
		{
			if (terminal_count < TERMINALS)
				id = terminal_count++;
			else
				id = TERMINALS - 1;
		}

		@Override
		public void handleEvent(Event e)
		{
			terminal_tasks++;
			time[3 + id][terminal_tasks - 1] = scheduler.currentTime();
		}

	}

	private class RandomDataCollector implements IListener
	{
		private int id;
		private int terminal_tasks = 0;

		RandomDataCollector()
		{
			if (random_count < TERMINALS)
				id = random_count++;
			else
				id = TERMINALS - 1;
		}

		@Override
		public void handleEvent(Event e)
		{
			terminal_tasks++;
			if (terminal_tasks <= TASKS)
			{
				randomTime[id][terminal_tasks - 1] = e.getData();
			}
		}
	};

	public static void main(String[] args) throws IOException
	{
		new Terminal();
	}

}
